/*
 * Copyright (c) 2015-present, Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 */

package com.facebook.drawee.controller;

import javax.annotation.Nullable;

import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicLong;

import android.content.Context;
import android.graphics.drawable.Animatable;

import com.facebook.common.internal.Lists;
import com.facebook.common.internal.Objects;
import com.facebook.common.internal.Preconditions;
import com.facebook.common.internal.Supplier;
import com.facebook.datasource.DataSource;
import com.facebook.datasource.DataSources;
import com.facebook.datasource.FirstAvailableDataSourceSupplier;
import com.facebook.datasource.IncreasingQualityDataSourceSupplier;
import com.facebook.drawee.components.RetryManager;
import com.facebook.drawee.interfaces.SimpleDraweeControllerBuilder;
import com.facebook.drawee.gestures.GestureDetector;
import com.facebook.drawee.interfaces.DraweeController;

/**
 * Base implementation for Drawee controller builders.
 */
public abstract class AbstractDraweeControllerBuilder<
        BUILDER extends AbstractDraweeControllerBuilder<BUILDER, REQUEST, IMAGE, INFO>,
        REQUEST,
        IMAGE,
        INFO>
        implements SimpleDraweeControllerBuilder {

    private static final ControllerListener<Object> sAutoPlayAnimationsListener =
            new BaseControllerListener<Object>() {
                @Override
                public void onFinalImageSet(String id, @Nullable Object info, @Nullable Animatable anim) {
                    if (anim != null) {
                        anim.start();
                    }
                }
            };

    private static final NullPointerException NO_REQUEST_EXCEPTION =
            new NullPointerException("No image request was specified!");

    // components
    private final Context mContext;
    private final Set<ControllerListener> mBoundControllerListeners;

    // builder parameters
    private
    @Nullable
    Object mCallerContext;
    private
    @Nullable
    REQUEST mImageRequest;
    private
    @Nullable
    REQUEST mLowResImageRequest;
    private
    @Nullable
    REQUEST[] mMultiImageRequests;
    private
    @Nullable
    Supplier<DataSource<IMAGE>> mDataSourceSupplier;
    private
    @Nullable
    ControllerListener<? super INFO> mControllerListener;
    private boolean mTapToRetryEnabled;
    private boolean mAutoPlayAnimations;
    // old controller to reuse
    private
    @Nullable
    DraweeController mOldController;

    private static final AtomicLong sIdCounter = new AtomicLong();

    protected AbstractDraweeControllerBuilder(
            Context context,
            Set<ControllerListener> boundControllerListeners) {
        mContext = context;
        mBoundControllerListeners = boundControllerListeners;
        init();
    }

    /**
     * Initializes this builder.
     */
    private void init() {
        mCallerContext = null;
        mImageRequest = null;
        mLowResImageRequest = null;
        mMultiImageRequests = null;
        mControllerListener = null;
        mTapToRetryEnabled = false;
        mAutoPlayAnimations = false;
        mOldController = null;
    }

    /**
     * Resets this builder to its initial values making it reusable.
     */
    public BUILDER reset() {
        init();
        return getThis();
    }

    /**
     * Sets the caller context.
     */
    @Override
    public BUILDER setCallerContext(Object callerContext) {
        mCallerContext = callerContext;
        return getThis();
    }

    /**
     * Gets the caller context.
     */
    @Nullable
    public Object getCallerContext() {
        return mCallerContext;
    }

    /**
     * Sets the image request.
     */
    public BUILDER setImageRequest(REQUEST imageRequest) {
        mImageRequest = imageRequest;
        return getThis();
    }

    /**
     * Gets the image request.
     */
    @Nullable
    public REQUEST getImageRequest() {
        return mImageRequest;
    }

    /**
     * Sets the low-res image request.
     */
    public BUILDER setLowResImageRequest(REQUEST lowResImageRequest) {
        mLowResImageRequest = lowResImageRequest;
        return getThis();
    }

    /**
     * Gets the low-res image request.
     */
    @Nullable
    public REQUEST getLowResImageRequest() {
        return mLowResImageRequest;
    }

    /**
     * Sets the array of first-available image requests that will be probed in order.
     * <p> For performance reasons, the array is not deep-copied, but only stored by reference.
     * Please don't modify once submitted.
     */
    public BUILDER setFirstAvailableImageRequests(REQUEST[] firstAvailableImageRequests) {
        mMultiImageRequests = firstAvailableImageRequests;
        return getThis();
    }

    /**
     * Gets the array of first-available image requests.
     * <p> For performance reasons, the array is not deep-copied, but only stored by reference.
     * Please don't modify.
     */
    @Nullable
    public REQUEST[] getFirstAvailableImageRequests() {
        return mMultiImageRequests;
    }

    /**
     * Sets the data source supplier to be used.
     * <p/>
     * <p/> Note: This is mutually exclusive with other image request setters.
     */
    public void setDataSourceSupplier(@Nullable Supplier<DataSource<IMAGE>> dataSourceSupplier) {
        mDataSourceSupplier = dataSourceSupplier;
    }

    /**
     * Gets the data source supplier if set.
     * <p/>
     * <p/>Important: this only returns the externally set data source (if any). Subclasses should
     * use {#code obtainDataSourceSupplier()} to obtain a data source to be passed to the controller.
     */
    @Nullable
    public Supplier<DataSource<IMAGE>> getDataSourceSupplier() {
        return mDataSourceSupplier;
    }

    /**
     * Sets whether tap-to-retry is enabled.
     */
    public BUILDER setTapToRetryEnabled(boolean enabled) {
        mTapToRetryEnabled = enabled;
        return getThis();
    }

    /**
     * Gets whether tap-to-retry is enabled.
     */
    public boolean getTapToRetryEnabled() {
        return mTapToRetryEnabled;
    }

    /**
     * Sets whether to auto play animations.
     */
    public BUILDER setAutoPlayAnimations(boolean enabled) {
        mAutoPlayAnimations = enabled;
        return getThis();
    }

    /**
     * Gets whether to auto play animations.
     */
    public boolean getAutoPlayAnimations() {
        return mAutoPlayAnimations;
    }

    /**
     * Sets the controller listener.
     */
    public BUILDER setControllerListener(ControllerListener<? super INFO> controllerListener) {
        mControllerListener = controllerListener;
        return getThis();
    }

    /**
     * Gets the controller listener
     */
    @Nullable
    public ControllerListener<? super INFO> getControllerListener() {
        return mControllerListener;
    }

    /**
     * Sets the old controller to be reused if possible.
     */
    @Override
    public BUILDER setOldController(@Nullable DraweeController oldController) {
        mOldController = oldController;
        return getThis();
    }

    /**
     * Gets the old controller to be reused.
     */
    @Nullable
    public DraweeController getOldController() {
        return mOldController;
    }

    /**
     * Builds the specified controller.
     */
    @Override
    public AbstractDraweeController build() {
        validate();

        // if only a low-res request is specified, treat it as a final request.
        if (mImageRequest == null && mMultiImageRequests == null && mLowResImageRequest != null) {
            mImageRequest = mLowResImageRequest;
            mLowResImageRequest = null;
        }

        return buildController();
    }

    /**
     * Validates the parameters before building a controller.
     */
    protected void validate() {
        Preconditions.checkState(
                (mMultiImageRequests == null) || (mImageRequest == null),
                "Cannot specify both ImageRequest and FirstAvailableImageRequests!");
        Preconditions.checkState(
                (mDataSourceSupplier == null) ||
                        (mMultiImageRequests == null && mImageRequest == null && mLowResImageRequest == null),
                "Cannot specify DataSourceSupplier with other ImageRequests! Use one or the other.");
    }

    /**
     * Builds a regular controller.
     */
    protected AbstractDraweeController buildController() {
        AbstractDraweeController controller = obtainController();
        maybeBuildAndSetRetryManager(controller);
        maybeAttachListeners(controller);
        return controller;
    }

    /**
     * Generates unique controller id.
     */
    protected static String generateUniqueControllerId() {
        return String.valueOf(sIdCounter.getAndIncrement());
    }

    /**
     * Gets the top-level data source supplier to be used by a controller.
     */
    protected Supplier<DataSource<IMAGE>> obtainDataSourceSupplier() {
        if (mDataSourceSupplier != null) {
            return mDataSourceSupplier;
        }

        Supplier<DataSource<IMAGE>> supplier = null;

        // final image supplier;
        if (mImageRequest != null) {
            supplier = getDataSourceSupplierForRequest(mImageRequest);
        } else if (mMultiImageRequests != null) {
            supplier = getFirstAvailableDataSourceSupplier(mMultiImageRequests);
        }

        // increasing-quality supplier; highest-quality supplier goes first
        if (supplier != null && mLowResImageRequest != null) {
            List<Supplier<DataSource<IMAGE>>> suppliers =
                    Lists.newArrayListWithCapacity(2);
            suppliers.add(supplier);
            suppliers.add(getDataSourceSupplierForRequest(mLowResImageRequest));
            supplier = IncreasingQualityDataSourceSupplier.create(suppliers);
        }

        // no image requests; use null data source supplier
        if (supplier == null) {
            supplier = DataSources.getFailedDataSourceSupplier(NO_REQUEST_EXCEPTION);
        }

        return supplier;
    }

    protected Supplier<DataSource<IMAGE>> getFirstAvailableDataSourceSupplier(
            REQUEST[] imageRequests) {
        List<Supplier<DataSource<IMAGE>>> suppliers =
                Lists.newArrayListWithCapacity(imageRequests.length * 2);
        // we first add cache-only suppliers, then the full-fetch ones
        for (int i = 0; i < imageRequests.length; i++) {
            suppliers.add(getDataSourceSupplierForRequest(imageRequests[i], /*cacheOnly */ true));
        }
        for (int i = 0; i < imageRequests.length; i++) {
            suppliers.add(getDataSourceSupplierForRequest(imageRequests[i]));
        }
        return FirstAvailableDataSourceSupplier.create(suppliers);
    }

    /**
     * Creates a data source supplier for the given image request.
     */
    protected Supplier<DataSource<IMAGE>> getDataSourceSupplierForRequest(REQUEST imageRequest) {
        return getDataSourceSupplierForRequest(imageRequest, /* bitmapCacheOnly */ false);
    }

    /**
     * Creates a data source supplier for the given image request.
     */
    protected Supplier<DataSource<IMAGE>> getDataSourceSupplierForRequest(
            final REQUEST imageRequest,
            final boolean bitmapCacheOnly) {
        final Object callerContext = getCallerContext();
        return new Supplier<DataSource<IMAGE>>() {
            @Override
            public DataSource<IMAGE> get() {
                return getDataSourceForRequest(imageRequest, callerContext, bitmapCacheOnly);
            }

            @Override
            public String toString() {
                return Objects.toStringHelper(this)
                        .add("request", imageRequest.toString())
                        .toString();
            }
        };
    }

    /**
     * Attaches listeners (if specified) to the given controller.
     */
    protected void maybeAttachListeners(AbstractDraweeController controller) {
        if (mBoundControllerListeners != null) {
            for (ControllerListener<? super INFO> listener : mBoundControllerListeners) {
                controller.addControllerListener(listener);
            }
        }
        if (mControllerListener != null) {
            controller.addControllerListener(mControllerListener);
        }
        if (mAutoPlayAnimations) {
            controller.addControllerListener(sAutoPlayAnimationsListener);
        }
    }

    /**
     * Installs a retry manager (if specified) to the given controller.
     */
    protected void maybeBuildAndSetRetryManager(AbstractDraweeController controller) {
        if (!mTapToRetryEnabled) {
            return;
        }
        RetryManager retryManager = controller.getRetryManager();
        if (retryManager == null) {
            retryManager = new RetryManager();
            controller.setRetryManager(retryManager);
        }
        retryManager.setTapToRetryEnabled(mTapToRetryEnabled);
        maybeBuildAndSetGestureDetector(controller);
    }

    /**
     * Installs a gesture detector to the given controller.
     */
    protected void maybeBuildAndSetGestureDetector(AbstractDraweeController controller) {
        GestureDetector gestureDetector = controller.getGestureDetector();
        if (gestureDetector == null) {
            gestureDetector = GestureDetector.newInstance(mContext);
            controller.setGestureDetector(gestureDetector);
        }
    }

    /* Gets the context. */
    protected Context getContext() {
        return mContext;
    }

    /**
     * Concrete builder classes should override this method to return a new controller.
     */
    protected abstract AbstractDraweeController obtainController();

    /**
     * Concrete builder classes should override this method to return a data source for the request.
     * <p/>
     * <p/>IMPORTANT: Do NOT ever call this method directly. This method is only to be called from
     * a supplier created in {#code getDataSourceSupplierForRequest(REQUEST, boolean)}.
     * <p/>
     * <p/>IMPORTANT: Make sure that you do NOT use any non-final field from this method, as the field
     * may change if the instance of this builder gets reused. If any such field is required, override
     * {#code getDataSourceSupplierForRequest(REQUEST, boolean)}, and store the field in a final
     * variable (same as it is done for callerContext).
     */
    protected abstract DataSource<IMAGE> getDataSourceForRequest(
            final REQUEST imageRequest,
            final Object callerContext,
            final boolean bitmapCacheOnly);

    /**
     * Concrete builder classes should override this method to return {#code this}.
     */
    protected abstract BUILDER getThis();
}
